package sbt.metamodel;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Map.Entry;

import sbt.metamodel.TraceListener.ChangeType;

/**
 * I provide an abstract base class for all traces.
 * @author pa
 *
 */
public abstract class AbstractTrace {
	
	/**
	 * {@value}
	 * 
	 * Used to define {@link #DATE_FORMAT}.
	 */
	public static final String DATE_FORMAT_VALUE =
			"yyyy-MM-dd'T'HH:mm:ss.SSSZ";  
	
	/**
	 * A {@link SimpleDateFormat} used to interpret trace origins.
	 * 
	 * @see #DATE_FORMAT_VALUE
	 * @see #getOriginAsDate()
	 */
	public static final DateFormat DATE_FORMAT =
			new SimpleDateFormat(DATE_FORMAT_VALUE);
	
	private long nextId, lastEnd;
	private NavigableMap<Long, Obsel> obselsById, obselsByBegin, obselsByEnd;
	private List<TraceListener> listeners;
	
	protected AbstractTrace() {
		nextId = 0;
		lastEnd = Long.MIN_VALUE;
		obselsById = new TreeMap<Long, Obsel>();
		obselsByBegin = new TreeMap<Long, Obsel>();
		obselsByEnd = new TreeMap<Long, Obsel>();
		listeners = new LinkedList<TraceListener>();
	}
	
	/**
	 * The model URI of this trace.
	 * 
	 * @return the model URI of this trace
	 */
	public abstract String getModel();

	/**
	 * The origin of this trace.
	 * 
	 * @return the origin of this trace (ISO date or opaque string)
	 */
	public abstract String getOrigin();
	
	/**
	 * The origin of this trace as a Date (if relevant), or null.
	 * <p>
	 * Uses {@link #DATE_FORMAT} for the conversion.
	 * 
	 * @return a Date, or null if this trace's origin is opaque.
	 */
	public Date getOriginAsDate() {
		try {
			return DATE_FORMAT.parse(getOrigin());
		}
		catch (ParseException ex) {
			return null;
		}
	}

	/**
	 * The obsels of this trace, in an { id -> Obsel } sorted map.
	 * <p>
	 * This is mostly useful for {@link TransformationMethod} implementations.
	 * 
	 * @return a sorted map of Obsels indexed by {@link Obsel#getId()}
	 * 
	 * @see #getObselsByBegin()
	 * @see #getObselsByEnd()
	 */
	public SortedMap<Long, Obsel> getObselsById() {
		return Collections.unmodifiableSortedMap(obselsById);
	}

	/**
	 * The obsels of this trace, in a { end -> Obsel } sorted map.
	 * <p>
	 * In order to simply enumerate the obsels, use the values() method
	 * of the returned map.
	 * 
	 * @return a sorted map of Obsels indexed by begin
	 *
	 * @see #getObselsByEnd()
	 */
	public SortedMap<Long, Obsel> getObselsByBegin() {
		return Collections.unmodifiableSortedMap(obselsByBegin);
	}

	/**
	 * The obsels of this trace, in a { end -> Obsel } sorted map.
	 * <p>
	 * In order to simply enumerate the obsels, use the values() method
	 * of the returned map.
	 * 
	 * @return a sorted map of Obsels indexed by end
	 * 
	 * @see #getObselsByBegin()
	 */
	public SortedMap<Long, Obsel> getObselsByEnd() {
		return Collections.unmodifiableSortedMap(obselsByEnd);
	}

	
	/**
     * Add a new Obsel to that trace.
     * 
     * @param type  see {@link Obsel#getType()}
     * @param begin  see {@link Obsel#getBegin()}
     * @param end  see {@link Obsel#getEnd()}
     * @param subject  see {@link Obsel#getSubject()}
     * @param attributes  see {@link Obsel#getAttributes()} (can be null)
     * @param related  see {@link Obsel#getRelated()} (can be null)
     * @param relating  see {@link Obsel#getRelating()} (can be null)
     * @return  the created Obsel
     */
    protected Obsel createObsel(String type, long begin, long end, String subject,
                Map<String, Object> attributes, Map<String, Obsel> related,
                Map<String, Obsel> relating) {
        Obsel ret = new Obsel(this, nextId, type, begin, end, subject,
        		attributes, related, relating);
        obselsById.put(nextId, ret);
        obselsByBegin.put(begin, ret);
        obselsByEnd.put(end, ret);
        nextId += 1;
    	ChangeType ctype = ChangeType.MONOTONIC_NONTEMP;
        if (end > lastEnd) {
            ctype = ChangeType.MONOTONIC_TEMPORAL;
        	lastEnd = end;
        }
        notifyAllObselChange(ctype);
        return ret;
    }
    
    /**
     * Register a {@link TraceListener} to this trace.
     * <p>
     * Has no effect if listener is already registered.
     * 
     * @param listener  the listener to register
     */
    public void register(TraceListener listener) {
    	listeners.add(listener);
    }
    
    /**
     * Unregister a {@link TraceListener} to this trace.
     * <p>
     * Has no effect if listener is not registered.
     * 
     * @param listener  the listener to register
     */
    public void unregister(TraceListener listener) {
    	listeners.remove(listener);
    }
   
    
   
    /**
     * Call {@link TraceListener#notifyMetadataChange(AbstractTrace)} on all
     * listeners registered to this trace.
     * 
     * @see #register(TraceListener)
     */
    protected void notifyAllMetadataChange() {
    	for (TraceListener l: listeners) {
    		l.notifyMetadataChange(this);
    	}
    }

    /**
     * Call {@link TraceListener#notifyObselsChange(AbstractTrace, ChangeType)}
     * on all listeners registered to this trace.
     * 
     * @see #register(TraceListener)
     */
    protected void notifyAllObselChange(ChangeType type) {
    	for (TraceListener l: listeners) {
    		l.notifyObselsChange(this, type);
    	}
    }
    
    /**
     * Empty this trace of all its obsels.
     */
    protected void clearObsels() {
    	obselsById.clear();
    	obselsByBegin.clear();
    	obselsByEnd.clear();
    	notifyAllObselChange(ChangeType.NON_MONOTONIC);
    }
    
    /**
     * A class for easily creating attribute map.
     * <p>
     * Exemple : <code>new Attr("foo", "FOO").and("bar", 42)</code>
     * <p>
     * NB: can also be used for parameters of
     * {@link TransformedTrace#TransformedTrace(TransformationMethod, java.util.Set, Map)}
     * @author pa
     *
     */
    public static class Attr extends HashMap<String, Object> {
		private static final long serialVersionUID = 1L;
		public Attr(String key, Object val) {
    		super();
    		put(key, val);
    	}
    	public Attr and(String key, Object val) {
    		put(key, val);
    		return this;
    	}
    }
    /**
     * A class for easily creating relation map.
     * <p>
     * Exemple : <code>new Rel("foo", obs1).and("bar", obs2)</code>
     * @author pa
     *
     */
    public static class Rel extends HashMap<String, Obsel> {
		private static final long serialVersionUID = 1L;
		public Rel(String key, Obsel val) {
    		super();
    		put(key, val);
    	}
    	public Rel and(String key, Obsel val) {
    		put(key, val);
    		return this;
    	}
    }
    
    /**
     * Return a turtle representation of this trace's obsels.
     * @return a Turtle string.
     */
    public String asTurtle() {
    	StringBuffer ret = new StringBuffer();;
    	ret.append("@prefix : <http://liris.cnrs.fr/silex/2009/ktbs#> .\n");
    	ret.append("@prefix m: <").append(getModel()).append("> .\n\n");
    	for (Obsel o: getObselsByBegin().values()) {
    		ret.append("<o").append(o.getId()).append(">");
    		ret.append(" a m:").append(o.getType()).append(";\n");
    		ret.append("    :hasTrace <.> ;\n");
    		ret.append("    :hasSubject \"").append(o.getSubject())
    			.append("\" ;\n");
    		ret.append("    :hasBegin ").append(o.getBegin()).append(" ;\n");
    		ret.append("    :hasEnd ").append(o.getEnd()).append(" ;\n");
    		for (Entry<String, Object> e: o.getAttributes().entrySet()) {
    			ret.append("    m:").append(e.getKey()).append(" ");
    			Object val = e.getValue();
    			if (val instanceof String) {
    				val = ((String)val)
    						.replaceAll("\n", "\\n").replaceAll("\"", "\\\"");
    				ret.append("\"").append(val).append("\"");
    			} else if (val instanceof Number
    					|| val instanceof Boolean ){
    				ret.append(val);
    			} else {
    				throw new MetamodelException("Don't know how to serialize "
    						+ val.getClass().getName());
    			}
    			ret.append(" ;\n");
    		}
    		for (Entry<String, Obsel> e: o.getRelated().entrySet()) {
    			ret.append("    m:").append(e.getKey()).append(" ");
    			ret.append("<o").append(e.getValue().getId()).append("> ;\n");
    		}
    		ret.append(".\n\n");
    	}
    	return ret.toString();
    }
}
